diff options
Diffstat (limited to 'src/app/(main)/websites/[websiteId]/segments')
8 files changed, 294 insertions, 0 deletions
diff --git a/src/app/(main)/websites/[websiteId]/segments/SegmentAddButton.tsx b/src/app/(main)/websites/[websiteId]/segments/SegmentAddButton.tsx new file mode 100644 index 0000000..7b70fee --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/segments/SegmentAddButton.tsx @@ -0,0 +1,21 @@ +import { useMessages } from '@/components/hooks'; +import { Plus } from '@/components/icons'; +import { DialogButton } from '@/components/input/DialogButton'; +import { SegmentEditForm } from './SegmentEditForm'; + +export function SegmentAddButton({ websiteId }: { websiteId: string }) { + const { formatMessage, labels } = useMessages(); + + return ( + <DialogButton + icon={<Plus />} + label={formatMessage(labels.segment)} + variant="primary" + width="800px" + > + {({ close }) => { + return <SegmentEditForm websiteId={websiteId} onClose={close} />; + }} + </DialogButton> + ); +} diff --git a/src/app/(main)/websites/[websiteId]/segments/SegmentDeleteButton.tsx b/src/app/(main)/websites/[websiteId]/segments/SegmentDeleteButton.tsx new file mode 100644 index 0000000..bb52a22 --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/segments/SegmentDeleteButton.tsx @@ -0,0 +1,60 @@ +import { ConfirmationForm } from '@/components/common/ConfirmationForm'; +import { useDeleteQuery, useMessages } from '@/components/hooks'; +import { Trash } from '@/components/icons'; +import { DialogButton } from '@/components/input/DialogButton'; +import { messages } from '@/components/messages'; + +export function SegmentDeleteButton({ + segmentId, + websiteId, + name, + onSave, +}: { + segmentId: string; + websiteId: string; + name: string; + onSave?: () => void; +}) { + const { formatMessage, labels, FormattedMessage } = useMessages(); + const { mutateAsync, isPending, error, touch } = useDeleteQuery( + `/websites/${websiteId}/segments/${segmentId}`, + ); + + const handleConfirm = async (close: () => void) => { + await mutateAsync(null, { + onSuccess: () => { + touch('segments'); + onSave?.(); + close(); + }, + }); + }; + + return ( + <DialogButton + icon={<Trash />} + title={formatMessage(labels.confirm)} + variant="quiet" + width="600px" + > + {({ close }) => ( + <ConfirmationForm + message={ + <FormattedMessage + {...messages.confirmRemove} + values={{ + target: <b>{name}</b>, + }} + /> + } + isLoading={isPending} + error={error} + onConfirm={handleConfirm.bind(null, close)} + onClose={close} + buttonLabel={formatMessage(labels.delete)} + buttonVariant="danger" + /> + )} + </DialogButton> + ); +} diff --git a/src/app/(main)/websites/[websiteId]/segments/SegmentEditButton.tsx b/src/app/(main)/websites/[websiteId]/segments/SegmentEditButton.tsx new file mode 100644 index 0000000..5c56cf1 --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/segments/SegmentEditButton.tsx @@ -0,0 +1,37 @@ +import { useMessages } from '@/components/hooks'; +import { Edit } from '@/components/icons'; +import { DialogButton } from '@/components/input/DialogButton'; +import type { Filter } from '@/lib/types'; +import { SegmentEditForm } from './SegmentEditForm'; + +export function SegmentEditButton({ + segmentId, + websiteId, + filters, +}: { + segmentId: string; + websiteId: string; + filters?: Filter[]; +}) { + const { formatMessage, labels } = useMessages(); + + return ( + <DialogButton + icon={<Edit />} + title={formatMessage(labels.segment)} + variant="quiet" + width="800px" + > + {({ close }) => { + return ( + <SegmentEditForm + segmentId={segmentId} + websiteId={websiteId} + filters={filters} + onClose={close} + /> + ); + }} + </DialogButton> + ); +} diff --git a/src/app/(main)/websites/[websiteId]/segments/SegmentEditForm.tsx b/src/app/(main)/websites/[websiteId]/segments/SegmentEditForm.tsx new file mode 100644 index 0000000..c3529d9 --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/segments/SegmentEditForm.tsx @@ -0,0 +1,86 @@ +import { + Button, + Form, + FormButtons, + FormField, + FormSubmitButton, + Label, + Loading, + TextField, +} from '@umami/react-zen'; +import { useMessages, useUpdateQuery, useWebsiteSegmentQuery } from '@/components/hooks'; +import { FieldFilters } from '@/components/input/FieldFilters'; +import { messages } from '@/components/messages'; + +export function SegmentEditForm({ + segmentId, + websiteId, + filters = [], + showFilters = true, + onSave, + onClose, +}: { + segmentId?: string; + websiteId: string; + filters?: any[]; + showFilters?: boolean; + onSave?: () => void; + onClose?: () => void; +}) { + const { data } = useWebsiteSegmentQuery(websiteId, segmentId); + const { formatMessage, labels, getErrorMessage } = useMessages(); + + const { mutateAsync, error, isPending, touch, toast } = useUpdateQuery( + `/websites/${websiteId}/segments${segmentId ? `/${segmentId}` : ''}`, + { + type: 'segment', + }, + ); + + const handleSubmit = async (formData: any) => { + await mutateAsync(formData, { + onSuccess: async () => { + toast(formatMessage(messages.saved)); + touch('segments'); + onSave?.(); + onClose?.(); + }, + }); + }; + + if (segmentId && !data) { + return <Loading placement="absolute" />; + } + + return ( + <Form + onSubmit={handleSubmit} + defaultValues={data || { parameters: { filters } }} + error={getErrorMessage(error)} + > + <FormField + name="name" + label={formatMessage(labels.name)} + rules={{ required: formatMessage(labels.required) }} + > + <TextField autoFocus={!segmentId} /> + </FormField> + {showFilters && ( + <> + <Label>{formatMessage(labels.filters)}</Label> + <FormField name="parameters.filters" rules={{ required: formatMessage(labels.required) }}> + <FieldFilters websiteId={websiteId} /> + </FormField> + </> + )} + <FormButtons> + <Button isDisabled={isPending} onPress={onClose}> + {formatMessage(labels.cancel)} + </Button> + <FormSubmitButton variant="primary" data-test="button-submit" isDisabled={isPending}> + {formatMessage(labels.save)} + </FormSubmitButton> + </FormButtons> + </Form> + ); +} diff --git a/src/app/(main)/websites/[websiteId]/segments/SegmentsDataTable.tsx b/src/app/(main)/websites/[websiteId]/segments/SegmentsDataTable.tsx new file mode 100644 index 0000000..c1ba82e --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/segments/SegmentsDataTable.tsx @@ -0,0 +1,24 @@ +import { DataGrid } from '@/components/common/DataGrid'; +import { useWebsiteSegmentsQuery } from '@/components/hooks'; +import { SegmentAddButton } from './SegmentAddButton'; +import { SegmentsTable } from './SegmentsTable'; + +export function SegmentsDataTable({ websiteId }: { websiteId?: string }) { + const query = useWebsiteSegmentsQuery(websiteId, { type: 'segment' }); + + const renderActions = () => { + return <SegmentAddButton websiteId={websiteId} />; + }; + + return ( + <DataGrid + query={query} + allowSearch={true} + autoFocus={false} + allowPaging={true} + renderActions={renderActions} + > + {({ data }) => <SegmentsTable data={data} />} + </DataGrid> + ); +} diff --git a/src/app/(main)/websites/[websiteId]/segments/SegmentsPage.tsx b/src/app/(main)/websites/[websiteId]/segments/SegmentsPage.tsx new file mode 100644 index 0000000..cbe7a1c --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/segments/SegmentsPage.tsx @@ -0,0 +1,16 @@ +'use client'; +import { Column } from '@umami/react-zen'; +import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls'; +import { Panel } from '@/components/common/Panel'; +import { SegmentsDataTable } from './SegmentsDataTable'; + +export function SegmentsPage({ websiteId }) { + return ( + <Column gap="3"> + <WebsiteControls websiteId={websiteId} allowFilter={false} allowDateFilter={false} /> + <Panel> + <SegmentsDataTable websiteId={websiteId} /> + </Panel> + </Column> + ); +} diff --git a/src/app/(main)/websites/[websiteId]/segments/SegmentsTable.tsx b/src/app/(main)/websites/[websiteId]/segments/SegmentsTable.tsx new file mode 100644 index 0000000..4dbe511 --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/segments/SegmentsTable.tsx @@ -0,0 +1,38 @@ +import { DataColumn, DataTable, type DataTableProps, Row } from '@umami/react-zen'; +import Link from 'next/link'; +import { SegmentDeleteButton } from '@/app/(main)/websites/[websiteId]/segments/SegmentDeleteButton'; +import { SegmentEditButton } from '@/app/(main)/websites/[websiteId]/segments/SegmentEditButton'; +import { DateDistance } from '@/components/common/DateDistance'; +import { useMessages, useNavigation } from '@/components/hooks'; + +export function SegmentsTable(props: DataTableProps) { + const { formatMessage, labels } = useMessages(); + const { websiteId, renderUrl } = useNavigation(); + + return ( + <DataTable {...props}> + <DataColumn id="name" label={formatMessage(labels.name)}> + {(row: any) => ( + <Link href={renderUrl(`/websites/${websiteId}?segment=${row.id}`, false)}> + {row.name} + </Link> + )} + </DataColumn> + <DataColumn id="created" label={formatMessage(labels.created)}> + {(row: any) => <DateDistance date={new Date(row.createdAt)} />} + </DataColumn> + <DataColumn id="action" align="end" width="100px"> + {(row: any) => { + const { id, name } = row; + + return ( + <Row> + <SegmentEditButton segmentId={id} websiteId={websiteId} /> + <SegmentDeleteButton segmentId={id} websiteId={websiteId} name={name} /> + </Row> + ); + }} + </DataColumn> + </DataTable> + ); +} diff --git a/src/app/(main)/websites/[websiteId]/segments/page.tsx b/src/app/(main)/websites/[websiteId]/segments/page.tsx new file mode 100644 index 0000000..0d3faac --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/segments/page.tsx @@ -0,0 +1,12 @@ +import type { Metadata } from 'next'; +import { SegmentsPage } from './SegmentsPage'; + +export default async function ({ params }: { params: Promise<{ websiteId: string }> }) { + const { websiteId } = await params; + + return <SegmentsPage websiteId={websiteId} />; +} + +export const metadata: Metadata = { + title: 'Segments', +}; |